//=============================================================================
// System  : MatchKit
// File    : IPatternExtensions.cs
// Author  : Fabio Bussu  (matchkit@fabiobussu.it)
// Updated : 04/16/2013
// Note    : Copyright 2013, Fabio Bussu, All rights reserved
//
// This code is published under the Microsoft Public License (Ms-PL). A copy
// of the license should be distributed with the code.  It can also be found
// at the project website: http://matchkit.codeplex.com. This notice, the
// author's name, and all copyright notices must remain intact in all
// applications, documentation, and source files.
//
// Version     Date     Who  Comments
// ============================================================================
// 1.0.0.0  04/16/2013   FB  First draft
//=============================================================================
using System;
using System.Collections.Generic;
using System.Text;
using MatchKit.Patterns;

namespace MatchKit
{
	/// <summary>
	/// Extends String type and IPattern inherited types with shortcut and utility methods.
	/// </summary>
	/// <threadsafety static="true" />
	/// <conceptualLink target="0a06b2a9-5f58-409a-ba62-639cfd77bca7" />
	public static class IPatternExtensions
	{
		/// <summary>
		/// Assign the capture name to a pattern.
		/// </summary>
		/// <param name="pattern">Pattern to assign.</param>
		/// <param name="name">Capture name.</param>
		/// <returns>The pattern instance, input of the method.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to assign a capture name in a functional-style way.
		/// </remarks>
		/// <example>
		///		<code lang="C#" source="..\MatchKit.Doc.Samples\PatternsSamples.cs" region="IPatternExtensions_CaptureAs" />
		/// </example>
		public static IPattern CaptureAs(
			#if EM
			this 
			#endif
			IPattern pattern, string name)
		{
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (name == null)
				throw new ArgumentNullException("name", "name cannot be null");

			pattern.CaptureName = name;
			return pattern;
		}

		/// <summary>
		/// Wrap a pattern with a new instance of <see cref="T:MatchKit.Patterns.OptionalPattern"/>.
		/// </summary>
		/// <param name="pattern">Pattern to make optional.</param>
		/// <returns>The optional pattern.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to write a more readable code. 
		/// The pattern in input is wrapped in <see cref="T:MatchKit.Patterns.OptionalPattern"/>.
		/// </remarks>
		/// <example>
		///		<code lang="C#" source="..\MatchKit.Doc.Samples\PatternsSamples.cs" region="IPatternExtensions_AsOptional" />
		/// </example>
		public static IPattern AsOptional(
			#if EM
			this 
			#endif
			IPattern pattern)
		{
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			return new OptionalPattern(pattern);
		}


		#region Match

		/// <summary>
		/// Performs a match on a content string of a pattern with default options.
		/// </summary>
		/// <param name="s">Content to match.</param>
		/// <param name="pattern">Pattern to match.</param>
		/// <returns>The resulted match.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to write a more readable code. 
		/// </remarks>
		public static Match Match(
			#if EM
			this 
			#endif
			string s, IPattern pattern)
		{
			return Match(s, pattern, new MatchOptions());
		}

		/// <summary>
		/// Performs a match on a content string of a pattern with the specified options.
		/// </summary>
		/// <param name="s">Content to match.</param>
		/// <param name="pattern">Pattern to match.</param>
		/// <param name="options">Options for the match context.</param>
		/// <returns>The resulted match.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to write a more readable code. 
		/// </remarks>
		public static Match Match(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			return pattern.Match(new MatchContext(s, options));
		}

		/// <summary>
		/// Performs an asynchronous match on a content string of a pattern with default options.
		/// </summary>
		/// <param name="s">Content to match.</param>
		/// <param name="pattern">Pattern to match.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMatch(
			#if EM
			this 
			#endif
			string s, IPattern pattern)
		{
			return BeginMatch(s, pattern, new MatchOptions(), null, null);
		}

		/// <summary>
		/// Performs an asynchronous match on a content string of a pattern with the specified options.
		/// </summary>
		/// <param name="s">Content to match.</param>
		/// <param name="pattern">Pattern to match.</param>
		/// <param name="options">Options for the match context.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMatch(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options)
		{
			return BeginMatch(s, pattern, options, null, null);
		}

		/// <summary>
		/// Performs an asynchronous match on a content string of a pattern with the specified options.
		/// </summary>
		/// <param name="s">Content to match.</param>
		/// <param name="pattern">Pattern to match.</param>
		/// <param name="options">Options for the match context.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMatch(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options, AsyncCallback callback, object obj)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var d = new Func<string, IPattern, MatchOptions, Match>(Match);
			return new MatchAsyncResult(d, d.BeginInvoke(s, pattern, options, callback, obj));
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Match operation.
		/// </summary>
		/// <param name="result">The async object returned by a <b>BeginMatch</b> method.</param>
		/// <returns>The resulted match.</returns>
		public static Match EndMatch(IAsyncResult result)
		{
			return EndMatch(null, result);
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Match operation.
		/// </summary>
		/// <param name="s">The content string. This value is ignored.</param>
		/// <param name="result">The async object returned by a <b>BeginMatch</b> method.</param>
		/// <returns>The resulted match.</returns>
		/// <remarks>
		/// This overlapping method exists only for completeness and to give the ability of write 
		/// code from a String type as for the <b>BeginMatch</b> method.
		/// </remarks>
		public static Match EndMatch(
			#if EM
			this 
			#endif
			string s, IAsyncResult result)
		{
			var matchAsyncResult = result as MatchAsyncResult;
			if (matchAsyncResult == null)
				throw new ArgumentException("Invalid async result", "result");
			return matchAsyncResult.EndMatch();
		}

		#endregion Match


		#region Find

		/// <summary>
		/// Performs a find on a content string of a pattern with default options.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <returns>An enumerable results.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to write a more readable code. 
		/// </remarks>
		public static IEnumerable<Match> Find(
			#if EM
			this 
			#endif
			string s, IPattern pattern)
		{
			return Find(s, pattern, new MatchOptions());
		}

		/// <summary>
		/// Performs a find on a content string of a pattern with the specfied options.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="options">Options for the match context.</param>
		/// <returns>An enumerable results.</returns>
		/// <remarks>
		/// This method is a shortcut that helps to write a more readable code. 
		/// </remarks>
		public static IEnumerable<Match> Find(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var context = new MatchContext(s, options);

			while (!context.IsEos)
			{
				var branch = context.Branch();
				var m = pattern.Match(branch);

				if (m.Success)
				{
					context.Commit(branch);
					yield return m;
				}
				else
				{
					context.NextCharacter();
				}
			}
		}

		private static Match[] FindAll(string s, IPattern pattern, MatchOptions options, Func<Match, bool> onMatchCallback)
		{
			var items = new List<Match>();
			foreach (var item in Find(s, pattern, options))
			{
				if (onMatchCallback != null)
				{
					if (!onMatchCallback(item))
						break;
				}
				items.Add(item);
			}
			return items.ToArray();
		}

		/// <summary>
		/// Performs an asynchronous find on a content string of a pattern with default options.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(
			#if EM
			this 
			#endif
			string s, IPattern pattern)
		{
			return BeginFind(s, pattern, new MatchOptions(), null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a content string of a pattern with the specified options.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="options">Options for the match context.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options)
		{
			return BeginFind(s, pattern, options, null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a content string of a pattern with the specified options.
		/// <br/>
		/// The <paramref name="onMatchCallback"/>, if specified, is invoked on every match found. If the callback 
		/// returns false the find process will be breaked.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="options">Options for the match context.</param>
		/// <param name="onMatchCallback">Callback invoked on every match. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options, Func<Match, bool> onMatchCallback)
		{
			return BeginFind(s, pattern, options, onMatchCallback, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a content string of a pattern with the specified options.
		/// <br/>
		/// The <paramref name="onMatchCallback"/>, if specified, is invoked on every match found. If the callback 
		/// returns false the find process will be breaked.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="options">Options for the match context.</param>
		/// <param name="onMatchCallback">Callback invoked on every match. This value can be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(
			#if EM
			this 
			#endif
			string s, IPattern pattern, MatchOptions options,
			Func<Match, bool> onMatchCallback, AsyncCallback callback, object obj)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var d = new Func<string, IPattern, MatchOptions, Func<Match, bool>, Match[]>(FindAll);
			return new FindAsyncResult(d, d.BeginInvoke(s, pattern, options, onMatchCallback, callback, obj));
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Find operation.
		/// </summary>
		/// <param name="result">The async object returned by the <b>BeginFind</b> method.</param>
		/// <returns>The resulted matches.</returns>
		public static Match[] EndFind(IAsyncResult result)
		{
			return EndFind(null, result);
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Find operation.
		/// </summary>
		/// <param name="s">The content string. This value is ignored.</param>
		/// <param name="result">The async object returned by a <b>BeginFind</b> method.</param>
		/// <returns>The resulted matches.</returns>
		/// <remarks>
		/// This overlapping method exists only for completeness and to give the ability of write 
		/// code from a String type as for the <b>BeginFind</b> method.
		/// </remarks>
		public static Match[] EndFind(
			#if EM
			this 
			#endif
			string s, IAsyncResult result)
		{
			var findAsyncResult = result as FindAsyncResult;
			if (findAsyncResult == null)
				throw new ArgumentException("Invalid async result", "result");
			return findAsyncResult.EndFind();
		}

		#endregion Find


		#region Replace

		/// <summary>
		/// Performs a replacement on a content string of a pattern with a fixed text.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="replaceWith">Replacement text for all matches found.</param>
		/// <returns>The replaced text.</returns>
		/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-ab0ac544dabc" />
		public static string Replace(
			#if EM
			this 
			#endif
			string s, IPattern pattern, string replaceWith)
		{
			return Replace(s, pattern, m => replaceWith);
		}

		/// <summary>
		/// Performs a replacement on a content string of a pattern with a replace callback.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="pattern">Pattern to find.</param>
		/// <param name="replaceWith">Replacement callback method invoked for each match found.</param>
		/// <returns>The replaced text.</returns>
		/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-ab0ac544dabc" />
		public static string Replace(
			#if EM
			this 
			#endif
			string s, IPattern pattern, Func<Match, string> replaceWith)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (pattern == null)
				throw new ArgumentNullException("pattern", "pattern cannot be null");
			if (replaceWith == null)
				throw new ArgumentNullException("replaceWith", "replaceWith cannot be null");

			var context = new MatchContext(s);
			var sb = new StringBuilder(s.Length);

			while (!context.IsEos)
			{
				var branch = context.Branch();
				var m = pattern.Match(branch);
				if (m.Success)
				{
					context.Commit(branch);
					sb.Append(replaceWith(m));
				}
				else
				{
					sb.Append(context.Current);
					context.NextCharacter();
				}
			}

			return sb.ToString();
		}

		/// <summary>
		/// Performs a replacement on a content string of an existing match with a fixed text.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="match">Match to replace.</param>
		/// <param name="replaceWith">Replacement text.</param>
		/// <returns>The replaced text.</returns>
		/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-ab0ac544dabc" />
		public static string Replace(
			#if EM
			this 
			#endif
			string s, Match match, string replaceWith)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (match == null)
				throw new ArgumentNullException("match", "match cannot be null");
			if (replaceWith == null)
				throw new ArgumentNullException("replaceWith", "replaceWith cannot be null");

			var loc = match.Location;
			return s.Substring(0, loc.Index)
				+ replaceWith
				+ s.Substring(loc.Index + match.MatchLength);
		}

		/// <summary>
		/// Performs a replacement on a content string of an existing matches collection with a fixed text.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="matches">Matches to replace.</param>
		/// <param name="replaceWith">Replacement text.</param>
		/// <returns>The replaced text.</returns>
		/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-ab0ac544dabc" />
		public static string Replace(
			#if EM
			this 
			#endif
			string s, IEnumerable<Match> matches, string replaceWith)
		{
			return Replace(s, matches, m => replaceWith);
		}

		/// <summary>
		/// Performs a replacement on a content string of an existing matches collection with a replace callback.
		/// </summary>
		/// <param name="s">Content to find.</param>
		/// <param name="matches">Matches to replace.</param>
		/// <param name="replaceWith">Replacement callback method invoked for each match of the collection.</param>
		/// <returns>The replaced text.</returns>
		/// <conceptualLink target="7e345cd8-5aaa-4616-a3c9-ab0ac544dabc" />
		public static string Replace(
			#if EM
			this 
			#endif
			string s, IEnumerable<Match> matches, Func<Match, string> replaceWith)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (matches == null)
				throw new ArgumentNullException("matches", "matches cannot be null");
			if (replaceWith == null)
				throw new ArgumentNullException("replaceWith", "replaceWith cannot be null");

			var sb = new StringBuilder(s.Length);
			int lastIndex = 0;
			foreach (var match in matches)
			{
				var loc = match.Location;
				sb.Append(s.Substring(lastIndex, loc.Index - lastIndex));
				sb.Append(replaceWith(match));
				lastIndex = loc.Index + match.MatchLength;
			}
			sb.Append(s.Substring(lastIndex, s.Length - lastIndex));
			return sb.ToString();
		}

		#endregion Replace
	}
}
